package com.learn.util;


import org.apache.ibatis.javassist.CannotCompileException;
import org.apache.ibatis.javassist.ClassPool;
import org.apache.ibatis.javassist.CtClass;
import org.apache.ibatis.javassist.CtMethod;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.session.SqlSession;

import java.lang.reflect.Method;
import java.util.Arrays;

/**
 * @author DJJ_F
 * @version 1.0
 * @date 2025/02/21
 * @description 这是为Dao接口动态生成代理类（实现类）的工具类
 * 注意：在mybatis中也集成了javassist包 package org.apache.ibatis.javassist 功能差不多是一样的，
 */
public class DaoProxyUtil {
    /**
     * 这是动态生成代理类, 实现类的方法
     *
     * @param anInterface 接口类对象
     * @return 返回实现类对象
     */
    public static Object genProxy(SqlSession sqlSession, Class<?> anInterface) {
        ClassPool pool = ClassPool.getDefault();
        CtClass ctInterface = pool.makeInterface(anInterface.getName());
        // 从 com.learn.dao.AccountDao -> com.learn.dao.AccountDaoProxy, 其实到哪都行
        CtClass ctClass = pool.makeClass(anInterface.getName() + "Proxy");
        ctClass.addInterface(ctInterface);
        Method[] methods = anInterface.getDeclaredMethods();

        // 处理方法
        Arrays.stream(methods).forEach(method -> {
            StringBuilder content = new StringBuilder("public ");
            content.append(method.getReturnType().getName() + " " + method.getName() + " ");
            content.append("(");
            // 开始处理参数列表
            Class<?>[] parameterTypes = method.getParameterTypes();
            for (int i = 0; i < parameterTypes.length; i++) {
                content.append(parameterTypes[i].getName() + " arg" + i);
                if (i != parameterTypes.length - 1) {
                    content.append(", ");
                }
            }
            content.append(") ");

            content.append("{");
            //开始处理方法体
            // 注意要使用全类名
            content.append("org.apache.ibatis.session.SqlSession sqlSession = com.learn.util.SqlSessionUtil.openSession();");
            /*
            直接return 可以避免return类型的问题，但是sqlSession要调用哪个方法update，insert,delete 还是select
            这时就需要用户传入一个SqlSession对象以获取要调用的方法的类型
            SqlCommandType是一个枚举类型包含各种sql语句类型
            但是这时我们需要得到sqlId才能知道他要调用的方法是什么类型的
            这时我们规定，namespace必须是全限定接口名，sqlId必须是接口中的方法名
            这样才能找到sqlId = namespace + 方法名，
            这非常重要，这也是为什么xml文件的namespace必须是接口全类名，id是方法名
            */
            String sqlId = anInterface.getName() + "." + method.getName();
            // 得到了sqlId 才能知道是什么类型， getConfiguration实际是对configuration根标签的封装，MappedStatement就是sql语句的封装
            SqlCommandType sqlCommandType = sqlSession.getConfiguration().getMappedStatement(sqlId).getSqlCommandType();
            // 枚举类型可以直接用==
            if (sqlCommandType == SqlCommandType.SELECT) {
                // 这里要强制类型转换不然会报错 Account to = accountDao.selectById(toId);
                content.append("return (" + method.getReturnType().getName() + ") sqlSession.selectOne(\"" + sqlId + "\", arg0); ");
            } else if (sqlCommandType == SqlCommandType.UPDATE) {
                content.append("return sqlSession.update(\"" + sqlId + "\", arg0);");
            }
            content.append("}");
            System.out.println(content);
            try {
                CtMethod ctMethod = CtMethod.make(content.toString(), ctClass);
                ctClass.addMethod(ctMethod);
            } catch (CannotCompileException e) {
                throw new RuntimeException(e);
            }
        });

        // 创建实现类对象并返回, 遇到问题就抛异常，也返回不了值了，所以可以在try外贸可以不写return
        try {
            Class<?> clazz = ctClass.toClass();
            return clazz.getDeclaredConstructor().newInstance();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}
